#! /usr/bin/python3

#load the libraries
import json
import random
import math
import numpy as np

#the number of users created yet (used for the userids)
userCounter = 0

#the object representing users
class User(object):
	
	#the creation of the object
	#parameter:
	#       attributes:     list of attributes for the user
	#       fingerprinter:  the fingerprinter to visit
	def __init__(self,attributes,fingerprinter):
		#list of attributes for the user
		self.attributes            = attributes
		#the fingerprinter to visit
		self.fingerprinter         = fingerprinter

		#increment the total number of users
		global userCounter
		userCounter  = userCounter + 1

		#set the userid
		self.userId  = userCounter
	
		#the activity log for the user
		self.log     = []

	#the callback function for the main simulation
	def tick(self):
		self.action()

	#do some tasks
	def action(self):
		self.__browse()
		
	#visit the fingerprinter
	def __browse(self):
		self.fingerprinter.visitedBy(self)
		self.log.append("visited fingerprinter")
			
#the object representing a fingerprintingscript
class Fingerprinter(object):

	#the creation of the object
	#parameter:
	#	fingerprintedAttributes: the list of attributes to meassure to obtain a fingerprint
	def __init__(self,fingerprintedAttributes):
		#the list of attributes to meassure to obtain a fingerprint
		self.__fingerprintedAttributes = fingerprintedAttributes

		#the fingerprints meassured
		self.messuredFingerprints = []

	#the function for beeing visited by an user
	#parameter:
	#	user: the user visiting
	def visitedBy(self,user):
		self.messuredFingerprints.append([user.userId,self.__extractFingerprint(user)])

	#measure the fingerprint for a visiting user
	#parameter:
	#       user: the user visiting
	def __extractFingerprint(self,user):
		result = []
		for index in self.__fingerprintedAttributes:
			result.append(user.attributes[index])
		return result

#the actual simulation of the fingerprinting process
class Simulation(object):

	#the creation of the object
	#parameter:
	#       numUsers:       the number of users to simulate
	def __init__(self,numUsers=1):
		#the number of users to simulate
		self.numUsers = numUsers

		#the fingerprinter for the users to visit
		self.__fingerprinter = Fingerprinter([0,1,2])

		#create the users
		self.__users = []
		for i in range(0,self.numUsers):
			self.__users.append(self.__generateUser())

	#run the simulation
	#parameter:
	#	ticks:  the number of ticks to simulate
	def run(self,ticks=1):
		#do all ticks
		for i in range(0,ticks):
			print("tick "+str(i+1))

			#do the tick on all users
			for user in self.__users:
				user.tick()

	#end the simulation
	#return:
	#	the data collected in the simulation
	def finish(self):
		import math

		#the container for the simulation results
		result = {}

		#the dict for counting the sizes of the anonymity sets
		counter = {}

		#populate the dict for counting the sizes of the anonymity sets
		for fingerprint in self.__fingerprinter.messuredFingerprints:
			if not str(fingerprint[1]) in counter:
				counter[str(fingerprint[1])] = 1
			else:
				counter[str(fingerprint[1])] += 1

		#add all fingerprints to the result
		result["raw"]		= self.__fingerprinter.messuredFingerprints

		#sort the anonymity sets by size
		sortedCounter = list(counter.values())
		sortedCounter.sort()

		#calculate the entopie of the measured fingerprints
		entropy = 0
		for anonSet in sortedCounter:
			probability = anonSet/self.numUsers
			entropy += probability*-math.log(probability,2)

		#calculate some statistics
		numUnique = 0

		result["Nutzer1"]	= 0
		result["Nutzer2"]	= 0
		result["Nutzer2-9"]	= 0
		result["Nutzer10+"]	= 0
		for anonSet in sortedCounter:
			if anonSet == 1:
				result["Nutzer1"] += 1
			if anonSet == 2:
				result["Nutzer2"] += 2
			if anonSet >= 2 and anonSet <= 9:
				result["Nutzer2-9"] += anonSet
			if anonSet >= 10:
				result["Nutzer10+"] += anonSet
		result["Nutzer1"]	/= self.numUsers
		result["Nutzer2"]	/= self.numUsers
		result["Nutzer2-9"]	/= self.numUsers
		result["Nutzer10+"]	/= self.numUsers

		result["num users"]	= self.numUsers
		result["anon1"]		= sortedCounter[-1]
		result["anon2"]		= sortedCounter[-2]
		result["anon3"]		= sortedCounter[-3]
		result["anon4"]		= sortedCounter[-4]
		result["anon5"]		= sortedCounter[-5]
		result["anon6"]		= sortedCounter[-6]
		result["anon7"]		= sortedCounter[-7]
		result["anon8"]		= sortedCounter[-8]
		result["anon9"]		= sortedCounter[-9]
		result["anon10"]	= sortedCounter[-10]
		result["Entropie"]	= entropy

		#return the result
		return result

	#generate a new user
	#return:
	#	the new user
	def __generateUser(self):
		#choose a mode for unique users or big anonymity sets
		if random.random() < 0.1:
			mode = "high"
		else:
			mode = "low"
	
		#generate the fingerprint for the new user
		if mode == "low":
			attributes = {}
			attributes[0] = 0
			attributes[1] = np.random.geometric(0.0000003)+1
			attributes[2] = 0
		elif mode == "high":
			attributes = {}
			attributes[0] = 1
			attributes[1] = 0
			attributes[2] = np.random.geometric(0.025)+1

		#return the new user
		return User(attributes,self.__fingerprinter)

#seed the random generators with a fixed seed
random.seed("KzxfB1rpkPZqgHap4SV87Ohw8ZwOhDXf")
np.random.seed(68236872)

#set the number of runs to do
numRuns = 1000

#container for the results
allData = []
finalResult = {}

#set up dummies for each part of the results
finalResult["Nutzer1"]		= 0
finalResult["Nutzer2"]		= 0
finalResult["Nutzer2-9"]	= 0
finalResult["Nutzer10+"]	= 0
finalResult["num users"]	= 0
finalResult["anon1"]		= 0
finalResult["anon2"]		= 0
finalResult["anon3"]		= 0
finalResult["anon4"]		= 0
finalResult["anon5"]		= 0
finalResult["anon6"]		= 0
finalResult["anon7"]		= 0
finalResult["anon8"]		= 0
finalResult["anon9"]		= 0
finalResult["anon10"]		= 0
finalResult["Entropie"]		= 0

#run the simulation and average the results
counter = 0
while (counter < numRuns):
	counter += 1

	#run the simulation with 470161 users and get its result
	simulation = Simulation(numUsers=470161)
	simulation.run()
	result = simulation.finish()

	#write the raw result
	data_dump = json.dumps(result)
	file = open("data/run"+str(counter),"w")
	file.write(data_dump)
	file.close()

	#free the memory from the unneeded details
	del result["raw"]	

	#add the round results to the complete result
	allData.append(result)

	finalResult["Nutzer1"]		+= result["Nutzer1"]
	finalResult["Nutzer2"]		+= result["Nutzer2"]
	finalResult["Nutzer2-9"]	+= result["Nutzer2-9"]
	finalResult["Nutzer10+"]	+= result["Nutzer10+"]
	finalResult["num users"]	+= result["num users"]
	finalResult["anon1"]		+= result["anon1"]
	finalResult["anon2"]		+= result["anon2"]
	finalResult["anon3"]		+= result["anon3"]
	finalResult["anon4"]		+= result["anon4"]
	finalResult["anon5"]		+= result["anon5"]
	finalResult["anon6"]		+= result["anon6"]
	finalResult["anon7"]		+= result["anon7"]
	finalResult["anon8"]		+= result["anon8"]
	finalResult["anon9"]		+= result["anon9"]
	finalResult["anon10"]		+= result["anon10"]
	finalResult["Entropie"]		+= result["Entropie"]

	#print some eyecandy
	print("")
	print("run: "+str(counter))
	print("")

#finish calculating the average
finalResult["Nutzer1"]		/= numRuns
finalResult["Nutzer2"]		/= numRuns
finalResult["Nutzer2-9"]	/= numRuns
finalResult["Nutzer10+"]	/= numRuns
finalResult["num users"]	/= numRuns
finalResult["anon1"]		/= numRuns
finalResult["anon2"]		/= numRuns
finalResult["anon3"]		/= numRuns
finalResult["anon4"]		/= numRuns
finalResult["anon5"]		/= numRuns
finalResult["anon6"]		/= numRuns
finalResult["anon7"]		/= numRuns
finalResult["anon8"]		/= numRuns
finalResult["anon9"]		/= numRuns
finalResult["anon10"]		/= numRuns
finalResult["Entropie"]		/= numRuns

#calculate the variance
varianz = {}

varianz["Nutzer1"]		= 0
varianz["Nutzer2"]		= 0
varianz["Nutzer2-9"]		= 0
varianz["Nutzer10+"]		= 0
varianz["num users"]		= 0
varianz["anon1"]		= 0
varianz["anon2"]		= 0
varianz["anon3"]		= 0
varianz["anon4"]		= 0
varianz["anon5"]		= 0
varianz["anon6"]		= 0
varianz["anon7"]		= 0
varianz["anon8"]		= 0
varianz["anon9"]		= 0
varianz["anon10"]		= 0
varianz["Entropie"]		= 0

counter = 0
while (counter < numRuns):

	varianz["Nutzer1"]		+= math.pow(allData[counter]["Nutzer1"]-finalResult["Nutzer1"],2)
	varianz["Nutzer2"]		+= math.pow(allData[counter]["Nutzer2"]-finalResult["Nutzer2"],2)
	varianz["Nutzer2-9"]		+= math.pow(allData[counter]["Nutzer2-9"]-finalResult["Nutzer2-9"],2)
	varianz["Nutzer10+"]		+= math.pow(allData[counter]["Nutzer10+"]-finalResult["Nutzer10+"],2)
	varianz["num users"]		+= math.pow(allData[counter]["num users"]-finalResult["num users"],2)
	varianz["anon1"]		+= math.pow(allData[counter]["anon1"]-finalResult["anon1"],2)
	varianz["anon2"]		+= math.pow(allData[counter]["anon2"]-finalResult["anon2"],2)
	varianz["anon3"]		+= math.pow(allData[counter]["anon3"]-finalResult["anon3"],2)
	varianz["anon4"]		+= math.pow(allData[counter]["anon4"]-finalResult["anon4"],2)
	varianz["anon5"]		+= math.pow(allData[counter]["anon5"]-finalResult["anon5"],2)
	varianz["anon6"]		+= math.pow(allData[counter]["anon6"]-finalResult["anon6"],2)
	varianz["anon7"]		+= math.pow(allData[counter]["anon7"]-finalResult["anon7"],2)
	varianz["anon8"]		+= math.pow(allData[counter]["anon8"]-finalResult["anon8"],2)
	varianz["anon9"]		+= math.pow(allData[counter]["anon9"]-finalResult["anon9"],2)
	varianz["anon10"]		+= math.pow(allData[counter]["anon10"]-finalResult["anon10"],2)
	varianz["Entropie"]		+= math.pow(allData[counter]["Entropie"]-finalResult["Entropie"],2)

	counter += 1

varianz["Nutzer1"]		/= numRuns
varianz["Nutzer2"]		/= numRuns
varianz["Nutzer2-9"]		/= numRuns
varianz["Nutzer10+"]		/= numRuns
varianz["num users"]		/= numRuns
varianz["anon1"]		/= numRuns
varianz["anon2"]		/= numRuns
varianz["anon3"]		/= numRuns
varianz["anon4"]		/= numRuns
varianz["anon5"]		/= numRuns
varianz["anon6"]		/= numRuns
varianz["anon7"]		/= numRuns
varianz["anon8"]		/= numRuns
varianz["anon9"]		/= numRuns
varianz["anon10"]		/= numRuns
varianz["Entropie"]		/= numRuns

#write somewhat detailed result
data_dump = json.dumps(allData)
file = open("data_eckersley_third_try.json","w")
file.write(data_dump)
file.close()

#write the result
finalResult["varianz"]		= varianz

#write somewhat detailed result
result_dump = json.dumps(finalResult)
file = open("results_eckersley_third_try.json","w")
file.write(result_dump)
file.close()

#print the result
print(result_dump)
